昨天我們提到了 this ,但礙於篇幅無法仔細描述,今天讓我們深入來看看 this 到底是什麼。
要講 this 就必須先介紹全域與區域變數的概念。其實,在 02 var、let、const 與 ES6 簡介中,我們有稍微提過這個概念,只是並沒有仔細描述。所謂的全域變數,就是直接 window 底下宣告,沒有放進函式裡宣告的變數。而區域變數,則是在函式裡頭宣告的變數,只有函式內可以使用,所以稱為區域。
廢話不多說,先來一題前菜:
var a = 1;
function b(){
var c = 2;
}
console.log(a);
console.log(c);
上面哪個是區域變數?哪個是全域變數?
答案是 a 是全域變數, c 是區域變數。所以如果我在 function b 外面要去運用 c 這個值,就會無法使用。舉例來說,上面的 console.log 最後只能得到 a 的答案,而 c 則會顯示錯誤訊息 c is not defined 。
讓我們看看下一題:
var a = 1;
function bar() {
console.log(a);
var a = 2;
console.log(a);
function bar_child () {
console.log(a);
}
function bar_child_2() {
var a = 3;
console.log(a);
}
bar_child();
bar_child_2();
}
function bar_2() {
console.log(a);
}
bar();
bar_2();
你覺得上面的 bar() 和 bar_2() 會得到什麼答案呢?別被這麼長的式子嚇著了,讓我們來分析一下整個結構。
首先是比較簡單的 bar_2() 。
function bar_2() {
console.log(a); // 印出 a
}
a 是多少呢? bar_2 本身區域中並沒有宣告,所以跳出來往上一看,發現最上面宣告了 var a = 1; 因為 bar_2 不能去拿別人家的 a ,只能拿外面全域的,因此這裡的 bar_2() 會得到 1 的答案。這題應該沒什麼問題,只要知道全域跟區域的概念就能回答了。讓我們來看看 bar() 。
var a = 1;
function bar() {
console.log(a);
var a = 2;
console.log(a);
function bar_child () {
console.log(a);
}
function bar_child_2() {
var a = 3;
console.log(a);
}
bar_child();
bar_child_2();
}
這裡總共有 4 次 console.log 。第一次,在它上面沒有宣告過 a 是誰,因此印出 undefined 。你可能會覺得很奇怪,欸不是,剛剛 function bar_2() 裡 console.log 上面也沒有宣告過 a 是誰啊,為什麼可以直接拿外面全域的 a ,這裡卻不能呢?
這是因為要優先取用區域變數裡的東西。而就在第一次的 console.log 下方,出現了宣告 a 是誰的句子。代表區域裡是有宣告的,所以不會去取全域的東西。當重複宣告時,就算先取用也只會顯示 undefined 。
第二次 console.log 答案是 2 ,這應該就沒什麼問題。上面宣告了 a = 2 ,這裡自然得到 2 的答案。
第三次跟 function bar_2() 這題有點像。在 function bar_child () 裡,並沒有宣告過 foo 是誰,所以就到他的父層,也就是 function bar() 裏頭去尋找,發現裡面宣告過 a = 2 ,因此第三次也會得到 2 的答案。
最後第四次應該不用我多說,答案會是 3 。
如果上面的例題,對你來說輕而易舉,那我們就可以進一步進入到 this 的世界。
先來寫一段程式:
function getGender(){
return this.gender;
};
let people1 = {
gender: 'female',
getGender: getGender
};
let people2 = {
gender: 'male',
getGender: getGender
};
console.log(people1.getGender());
console.log(people2.getGender());
This 跟英文一樣,假設你講一段話,每次都要講一次這是阿明的同事的外婆的項鍊,那舌頭可能講完三次就直接打結起水泡。當上面的 function ,要隨下方不同人跳出不同結果時,為避免每次 return 都要逐一寫 return peoplexx.gender ,可利用 this 來解決。
但是要注意全域變數的問題。上面的範例中, gender 存在在物件(也就是區域變數)中,因此在 getGender 時,會取到正確的數字。但如果你要呼叫的東西,不在物件中,而是本身就是個物件呢?在下面的例子中,你會發現這將導致取到全域變數(也就是window)的值,而顯示不出原先設計時想跑出的效果:
var foo = function() {
this.count++;
};
foo.count = 0;
for(var i = 0; i < 5; i++) {
foo();
}
上面的例子中,原本是希望讓 for 迴圈跑完以後,把 foo 的值送出去。但 foo 本身就是函式,宣告的時候放在全域中,因此是全域變數。導致下面呼叫 foo 時,實際上是讓 this 呼叫到 window ,偏偏 window 中從頭到尾,沒有寫一行 window.count 。 undefined 加了 5 次,怎麼會有 undefined 加法呢?最後跑出的結果當然會是 NaN 。
foo.count 則會一直維持在 0 的狀態了。
這個例子我花了很久的時間才參透其中意義。舉另一個較簡單的例子來說:
var foo = 1;
console.log(window.foo);
以上的程式會跑出 1 的結果。整段的重點是 foo 在全域中宣告,因此 console.log 的括號中要寫 window.foo 。相信熟悉全域區域觀念的你,可以立刻發現,同理,上面第一個例子也隱藏類似觀念。
綜合以上可知,要用 this 呼叫的東西必須存放在物件(也就是區域變數)中。 this 代表的是 function 執行時所屬的物件,不是 function 本身。
所以當程式這樣寫:
var foo = function() {
this.count++;
};
this 指的不是 function foo 本身,而是 function foo 執行時所屬的物件。也就是 window ,因為 foo 放在全域中,所以隸屬在 window 下面。如果你看懂了這句,再回去看上面的例題也許會更加明白其中的奧義。